﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Windows.Win32.Foundation;
using Windows.Win32.Security.Cryptography;
using Windows.Win32.Security.WinTrust;
using static Windows.Win32.PInvoke;

namespace Microsoft.DotNet.Cli.Installer.Windows.Security;

/// <summary>
/// Contains methods for verifying Authenticode signatures on Windows.
/// </summary>
#if NETCOREAPP
[SupportedOSPlatform("windows5.1.2600")]
#endif
internal static class Signature
{
    /// <summary>
    /// Verifies that the certificate used to sign the specified file terminates in a trusted Microsoft root certificate. The policy check is performed against
    /// the first simple chain. 
    /// </summary>
    /// <param name="path">The path of the file to verify.</param>
    /// <returns>0 if the policy could be checked.<see cref="Marshal.GetPInvokeErrorMessage(int)"/> can be called to obtain more detail about the failure.</returns>
    /// <exception cref="CryptographicException"/>
    /// <remarks>This method does not perform any other chain validation like revocation checks, timestamping, etc.</remarks>
    internal static unsafe int HasMicrosoftTrustedRoot(string path)
    {
        var certContentType = X509Certificate2.GetCertContentType(path);
        if (certContentType != X509ContentType.Authenticode)
        {
            throw new CryptographicException($"Unexpected certificate content type, got '{certContentType}' instead of Authenticode.");
        }

        // Create an X509Certificate2 instance so we can access the certificate context and create a chain context.
#pragma warning disable SYSLIB0057 // we need Authenticode support which isn't available from X509CertificateLoader
        using X509Certificate2 certificate = new(path);
#pragma warning restore SYSLIB0057

        // We don't use X509Chain because it doesn't support verifying the specific policy and because we defer
        // that to the WinTrust provider as it performs timestamp and revocation checks.
        HCERTCHAINENGINE HCCE_LOCAL_MACHINE = (HCERTCHAINENGINE)0x01;
        CERT_CHAIN_PARA pChainPara = default;
        CERT_CHAIN_CONTEXT* pChainContext = default;
        CERT_CONTEXT* pCertContext = (CERT_CONTEXT*)certificate.Handle;
        uint dwFlags = 0;

        try
        {
            if (!CertGetCertificateChain(HCCE_LOCAL_MACHINE, pCertContext, null, default, &pChainPara, dwFlags, null, &pChainContext))
            {
                throw new CryptographicException(Marshal.GetPInvokeErrorMessage(Marshal.GetLastWin32Error()));
            }

            CERT_CHAIN_POLICY_PARA policyCriteria = default;
            CERT_CHAIN_POLICY_STATUS policyStatus = default;

            policyCriteria.cbSize = (uint)sizeof(CERT_CHAIN_POLICY_PARA);
            policyCriteria.dwFlags = (CERT_CHAIN_POLICY_FLAGS)MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG;

            if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_MICROSOFT_ROOT, pChainContext, &policyCriteria, &policyStatus))
            {
                throw new CryptographicException(string.Format(CliStrings.UnableToCheckCertificateChainPolicy, nameof(CERT_CHAIN_POLICY_MICROSOFT_ROOT)));
            }

            return (int)policyStatus.dwError;
        }
        finally
        {
            CertFreeCertificateChain(pChainContext);
        }
    }

    /// <summary>
    /// Verifies that the specified file is Authenticode signed.
    /// </summary>
    /// <param name="path">The path of the file to verify.</param>
    /// <param name="allowOnlineRevocationChecks">Allow revocation checks to go online. When set to <see langword="false"/>, the cached certificate revocation list is used instead.</param>
    /// <returns>0 if successful. <see cref="Marshal.GetPInvokeErrorMessage(int)"/> can be called to obtain more detail about the failure.</returns>
    /// <remarks>See this <see href="https://learn.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--verifying-the-signature-of-a-pe-file">example</see> for more information.
    /// A valid Authenticode signature does not establish trust. For example, Microsoft SHA1 signatures will return a positive result, even though their
    /// root certificates are no longer trusted. This simply verifies that the Authenticode signature is valid.
    /// </remarks>
    internal static unsafe int IsAuthenticodeSigned(string path, bool allowOnlineRevocationChecks = true)
    {
        fixed (char* p = Path.GetFullPath(path))
        {
            WINTRUST_FILE_INFO fileInfo = new()
            {
                pcwszFilePath = p,
                cbStruct = (uint)sizeof(WINTRUST_FILE_INFO),
            };

            Guid policyGuid = WINTRUST_ACTION_GENERIC_VERIFY_V2;

            WINTRUST_DATA trustData = new()
            {
                cbStruct = (uint)sizeof(WINTRUST_DATA),
                dwUIChoice = WINTRUST_DATA_UICHOICE.WTD_UI_NONE,
                fdwRevocationChecks = WINTRUST_DATA_REVOCATION_CHECKS.WTD_REVOKE_WHOLECHAIN,
                dwUnionChoice = WINTRUST_DATA_UNION_CHOICE.WTD_CHOICE_FILE,
                dwStateAction = WINTRUST_DATA_STATE_ACTION.WTD_STATEACTION_VERIFY,
                dwProvFlags = WINTRUST_DATA_PROVIDER_FLAGS.WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT,
            };

            if (!allowOnlineRevocationChecks)
            {
                trustData.dwProvFlags |= WINTRUST_DATA_PROVIDER_FLAGS.WTD_CACHE_ONLY_URL_RETRIEVAL;
            }

            trustData.Anonymous.pFile = &fileInfo;

            int lstatus = WinVerifyTrust((HWND)nint.Zero, ref policyGuid, &trustData);

            // Release the hWVTStateData handle, but return the original status.
            trustData.dwStateAction = WINTRUST_DATA_STATE_ACTION.WTD_STATEACTION_CLOSE;
            _ = WinVerifyTrust((HWND)nint.Zero, ref policyGuid, &trustData);

            return lstatus;
        }
    }
}
